package goobj

import (
	"fmt"
	"os"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"loov.dev/lensm/internal/disasm"
	godisasm "loov.dev/lensm/internal/go/src/disasm"
)

var rxRefAbs = regexp.MustCompile(`\s0x[\da-fA-F]+$`)
var rxRefRel = regexp.MustCompile(`\s-?\d+\(PC\)$`)
var rxCall = regexp.MustCompile(`^CALL\s+([\w\d\/\.\(\)\*]+)\(SB\)`)

// Disassemble disassembles the specified symbol.
func Disassemble(dis *godisasm.Disasm, sym *Function, opts disasm.Options) (*disasm.Code, error) {
	neededLines := make(map[string]*disasm.LineSet)

	file, _, _ := dis.PCLN().PCToLine(sym.sym.Addr)
	needRefPCs := map[uint64]struct{}{}

	code := &disasm.Code{
		Name: sym.Name(),
		File: file,
	}
	var instructions []disasm.Inst
	dis.Decode(sym.sym.Addr, sym.sym.Addr+uint64(sym.sym.Size), sym.sym.Relocs, false,
		func(pc, size uint64, file string, line int, text string) {
			// TODO: find a better way to calculate the jump target
			var refPC uint64
			var call string
			if match := rxRefAbs.FindString(text); match != "" {
				if target, err := strconv.ParseInt(match[3:], 16, 64); err == nil {
					refPC = uint64(target)
				}
			} else if match := rxRefRel.FindString(text); match != "" {
				// TODO: this calculation seems incorrect
				if target, err := strconv.ParseInt(match[1:len(match)-4], 10, 64); err == nil {
					refPC = uint64(int64(pc) + target*4)
				} else {
					panic(err)
				}
			} else if match := rxCall.FindStringSubmatch(text); len(match) > 0 {
				call = match[1]
			}

			if refPC != 0 {
				needRefPCs[refPC] = struct{}{}
			}
			instructions = append(instructions, disasm.Inst{
				PC:    pc,
				Text:  text,
				File:  file,
				Line:  line,
				Call:  call,
				RefPC: refPC,
			})

			if file != "" && file != "<autogenerated>" {
				lineset, ok := neededLines[file]
				if !ok {
					lineset = &disasm.LineSet{}
					neededLines[file] = lineset
				}
				lineset.Add(line)
			}
		})

	pcToIndex := map[uint64]int{}
	for _, ix := range instructions {
		if _, ok := needRefPCs[ix.PC]; ok {
			// add empty line
			code.Insts = append(code.Insts, disasm.Inst{})
		}
		pcToIndex[ix.PC] = len(code.Insts)
		code.Insts = append(code.Insts, ix)
	}

	type jumpInterval struct {
		index    int
		ix       *disasm.Inst
		min, max uint64
	}

	var jumps []jumpInterval
	for i := range code.Insts {
		ix := &code.Insts[i]
		if ix.RefPC != 0 {
			target, ok := pcToIndex[ix.RefPC]
			if !ok {
				continue
			}
			ix.RefOffset = target - i

			if ix.PC <= ix.RefPC {
				jumps = append(jumps, jumpInterval{
					index: i,
					ix:    ix,
					min:   ix.PC,
					max:   ix.RefPC,
				})
			} else {
				jumps = append(jumps, jumpInterval{
					index: i,
					ix:    ix,
					min:   ix.RefPC,
					max:   ix.PC,
				})
			}
		}
	}

	sort.Slice(jumps, func(i, k int) bool {
		if jumps[i].min == jumps[k].min {
			return jumps[i].max > jumps[k].max
		}
		return jumps[i].min < jumps[k].min
	})

	var stackLayers []uint64
	insertToStack := func(ix *disasm.Inst, max uint64) {
		found := false
		for k, pc := range stackLayers {
			if pc == 0 {
				stackLayers[k] = max
				ix.RefStack = k
				found = true
				break
			}
		}
		if !found {
			code.MaxJump = len(stackLayers)
			ix.RefStack = len(stackLayers)
			stackLayers = append(stackLayers, max)
		}
	}

	for _, jump := range jumps {
		for i, pc := range stackLayers {
			if pc <= jump.min {
				stackLayers[i] = 0
			}
		}
		insertToStack(jump.ix, jump.max)
	}
	for i := range code.Insts {
		ix := &code.Insts[i]
		ix.RefStack = code.MaxJump - ix.RefStack + 1
	}
	code.MaxJump++

	// remove trailing interrupts from funcs
	for len(code.Insts) > 0 &&
		(strings.HasPrefix(code.Insts[len(code.Insts)-1].Text, "INT ") ||
			code.Insts[len(code.Insts)-1].Text == "?") {
		code.Insts = code.Insts[:len(code.Insts)-1]
	}

	// load sources
	code.Source = LoadSources(neededLines, code.File, opts.Context)

	// create a mapping from source code to disassembly
	type fileLine struct {
		file string
		line int
	}

	lineRefs := map[fileLine]*disasm.LineSet{}
	for i, ix := range code.Insts {
		k := fileLine{file: ix.File, line: ix.Line}
		n, ok := lineRefs[k]
		if !ok {
			n = &disasm.LineSet{}
			lineRefs[k] = n
		}
		n.Add(i)
	}
	for i := range code.Source {
		src := &code.Source[i]
		for k := range src.Blocks {
			block := &src.Blocks[k]
			block.Related = make([][]disasm.LineRange, len(block.Lines))
			for line := block.From; line <= block.To; line++ { // todo check: line <= block.To
				if refs, ok := lineRefs[fileLine{file: src.File, line: line}]; ok {
					block.Related[line-block.From] = refs.RangesZero()
				}
			}
		}
	}

	return code, nil
}

var rxEnvVariable = regexp.MustCompile(`\$[a-zA-Z_]+[a-zA-Z0-9_]+\b`)

func replaceEnvironmentVariables(s string) string {
	return rxEnvVariable.ReplaceAllStringFunc(s, func(env string) string {
		replacement := os.Getenv(env[1:])
		if replacement != "" {
			return replacement
		}
		return env
	})
}

// LoadSources loads the specified line sets.
func LoadSources(needed map[string]*disasm.LineSet, symbolFile string, context int) []disasm.Source {
	var sources []disasm.Source
	for file, set := range needed {
		data, err := os.ReadFile(replaceEnvironmentVariables(file))
		if err != nil {
			// TODO: should we create a stub source block instead?
			fmt.Fprintf(os.Stderr, "unable to load source from %q: %v\n", file, err)
			continue
		}
		lines := strings.Split(string(data), "\n")
		source := disasm.Source{
			File: file,
		}
		for _, r := range set.Ranges(context) {
			to := r.To - 1
			if to > len(lines) {
				to = len(lines)
			}
			lineBlock := lines[r.From-1 : to]
			for i, v := range lineBlock {
				lineBlock[i] = strings.Replace(v, "\t", "    ", -1)
			}

			source.Blocks = append(source.Blocks, disasm.SourceBlock{
				LineRange: r,
				Lines:     lineBlock,
			})
		}
		sources = append(sources, source)
	}

	// Sort the sources and prioritize the file where the main symbol is located.
	sort.Slice(sources, func(i, j int) bool {
		if sources[i].File == symbolFile {
			return true
		}
		if sources[j].File == symbolFile {
			return false
		}
		return sources[i].File < sources[j].File
	})

	return sources
}
